Skip to content

fix(toast): top-layer container — closes #71#74

Merged
Goosterhof merged 3 commits into
mainfrom
feature/issue-71-toast-top-layer
May 8, 2026
Merged

fix(toast): top-layer container — closes #71#74
Goosterhof merged 3 commits into
mainfrom
feature/issue-71-toast-top-layer

Conversation

@Goosterhof
Copy link
Copy Markdown
Contributor

Summary

Closes #71 — "Toast onder blur van modal" (toasts rendered beneath the fs-dialog modal backdrop in kendo).

fs-dialog opens via native <dialog>.showModal(), which promotes the dialog and its ::backdrop to the browser top layer. No z-index value can pierce the top layer, so fs-toast's plain-DOM container rendered beneath the blurred backdrop. This PR migrates the container to the Popover API (popover="manual" + .showPopover() / .hidePopover()), the only DOM mechanism (besides another <dialog show modal>) that places an element in the top layer alongside an open modal.

Behavior

  • The container declares popover="manual" on its single root <div>.
  • setup() watches toasts.value.length: 0 → ≥ 1 calls .showPopover(); ≥ 1 → 0 calls .hidePopover().
  • An internal isOpen flag prevents redundant .showPopover() calls during multi-toast bursts and redundant .hidePopover() during intermediate removals.
  • try/catch around both calls swallows InvalidStateError (rapid show/hide cycles, older browsers).
  • onMounted promotes the container if a service has toasts before mount.
  • Single-root output preserved (0.1.1 fragment fix). Fallthrough class/style attributes still land on the root.

Migration — CSS Specificity

The UA stylesheet applies position: fixed; inset: 0; margin: auto; width/height: fit-content to [popover]:popover-open at specificity (0,2,0). Consumer fallthrough classes like .toast-stack { position: fixed; top: 1rem; right: 1rem } ((0,1,0)) do not override it. Consumers must qualify their selector or use !important:

[popover].toast-stack {
    position: fixed;
    top: 1rem;
    right: 1rem;
    inset: auto;
    margin: 0;
    width: auto;
    height: auto;
}

fs-toast deliberately ships no inline style resets — inline style would block consumer overrides entirely. The migration note appears in both packages/toast/CHANGELOG.md and docs/packages/toast.md.

Browser Baseline

Popover API support: Chrome ≥ 114, Firefox ≥ 125, Safari ≥ 17. Older browsers fall through the defensive try/catch — toasts still render in normal DOM (just without top-layer promotion, so they will render below modal backdrops on those browsers).

Version

0.1.10.2.0 (minor — additive behavior plus consumer-visible CSS specificity migration).

No internal fs-packages consumers depend on fs-toast, so no peer-range cascade is required. package-lock.json regenerates cleanly with fs-toast resolving to the workspace symlink (no nested registry copy).

CI Gates (all 8 passed locally)

  • npm audit — 0 vulnerabilities
  • npm run format:check — clean (133 files)
  • npm run lint — 0 warnings, 0 errors (95 rules)
  • npm run build — tsdown ESM + CJS for all 10 packages
  • npm run typecheck — clean across all packages
  • npm run lint:pkg — publint + attw zero advisories on all 10 packages
  • npm run test:coverage — 448 / 448 tests pass; fs-toast 100% coverage (47/47 stmts, 19/19 branches, 13/13 funcs, 39/39 lines)
  • npx stryker run (in packages/toast) — fs-toast 98.36% mutation score (60 killed / 1 survived; threshold 90%). The one survivor is pre-existing on hide()'s index === -1 short-circuit, unrelated to this change.

Test plan

  • popover="manual" attribute present on container root
  • showPopover called when first toast added (0 → 1)
  • showPopover NOT called repeatedly while popover is already open (1 → 2 → 3)
  • hidePopover called when last toast removed (1 → 0)
  • hidePopover NOT called when intermediate toast removed (3 → 2)
  • showPopover called again after a hide → show cycle
  • InvalidStateError swallowed on showPopover (rapid re-show)
  • InvalidStateError swallowed on hidePopover (rapid re-hide)
  • After thrown showPopover, subsequent transition retries (and hide-during-not-open is a no-op)
  • onMounted promotes container when service already has toasts pre-mount
  • Single-root preserved: fallthrough class + data-attribute land on the root <div> alongside popover="manual"
  • All existing 0.1.1 tests pass without modification

happy-dom note

happy-dom does not implement the Popover API natively (verified empirically — typeof showPopover === 'undefined'). Per orders, showPopover/hidePopover are stubbed on HTMLElement.prototype with idempotent ??= assignment at file scope, then wrapped per-test with vi.spyOn (cleared via vi.restoreAllMocks() in afterEach to prevent prototype-spy accumulation). Test runtime stays on happy-dom — no jsdom switch.

Out of Scope

  • Consumer-side CSS migration in kendo — separate kendo-territory deployment after this lands and republishes.
  • fs-dialog — unchanged; the dialog package was correct.
  • Auto-dismiss timers, keyboard handling, focus management — fs-toast remains component-agnostic; consumer-supplied toast component owns all UX behavior.

Goosterhof added 3 commits May 8, 2026 11:45
Toasts rendered beneath fs-dialog modal backdrops because the container
was a plain <div> in normal DOM, while <dialog>.showModal() promotes the
dialog and its ::backdrop to the browser top layer (no z-index pierces).

Migrate the container to the Popover API (popover="manual"). The setup()
function tracks the queue with watch(toasts.value.length, ...): when the
queue gains its first toast call .showPopover(), when it empties call
.hidePopover(). onMounted promotes if a service has toasts before mount.

Defensive guards:
- Internal isOpen flag prevents redundant showPopover() calls during
  multi-toast bursts and redundant hidePopover() during intermediate
  removals (preserves "popover already open" / "already closed" semantics
  without relying on :popover-open matching, which happy-dom does not
  implement).
- try/catch around both calls swallows InvalidStateError so rapid
  show/hide cycles or older browsers do not surface uncaught errors.

Single-root <div> output preserved (0.1.1 fragment fix). Fallthrough
class/style attributes still land on the container element. No inline
style resets — UA :popover-open specificity is documented in CHANGELOG
and docs as a consumer migration step rather than papered over.

Tests: 11 new assertions in tests/toast.spec.ts cover popover attribute
presence, fallthrough preservation, show/hide call shape, no-repeat-show
while open, no-hide-on-intermediate-removal, retry after thrown
showPopover, mount-with-pretoast, and InvalidStateError swallow on both
sides. happy-dom lacks the Popover API natively — methods stubbed on
HTMLElement.prototype with vi.spyOn per-test (afterEach restores to
prevent prototype-spy accumulation across tests).

100% coverage maintained. Stryker mutation: 98.36% (60/61 killed; one
pre-existing equivalent on hide() index===-1 short-circuit, unrelated
to this change).

Closes #71
Minor bump: top-layer promotion is additive container behavior plus a
consumer-visible CSS specificity migration step. UA stylesheet rules for
[popover]:popover-open (specificity 0,2,0) override consumer fallthrough
positioning classes (specificity 0,1,0); CHANGELOG documents the
mechanical override (qualify with [popover].toast-stack or use
!important) and the browser baseline (Chrome 114+, Firefox 125+,
Safari 17+).

No internal fs-packages consumers depend on fs-toast, so no peer-range
cascade is required. package-lock.json regenerates cleanly with
fs-toast resolving to the workspace symlink (no nested registry copy).
New "Top-Layer Behavior (0.2.0+)" section on docs/packages/toast.md:
- explains the Popover API mechanism with link to MDN's primer
- captures the CSS specificity gotcha (UA :popover-open overrides
  consumer fallthrough positioning) with a copy-paste-ready
  [popover].toast-stack example
- states the browser baseline and the older-browser fall-through
  behavior (try/catch swallows missing-method errors; toasts still
  render, just without modal coexistence)

Mirrors the migration note in CHANGELOG so consumers reading either
surface get the same guidance.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying fs-packages with  Cloudflare Pages  Cloudflare Pages

Latest commit: f5deaed
Status: ✅  Deploy successful!
Preview URL: https://48e54958.fs-packages.pages.dev
Branch Preview URL: https://feature-issue-71-toast-top-l.fs-packages.pages.dev

View logs

@Goosterhof Goosterhof merged commit f51e8b2 into main May 8, 2026
2 checks passed
@Goosterhof Goosterhof deleted the feature/issue-71-toast-top-layer branch May 8, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Toast onder blur van modal

2 participants